#!/usr/bin/env python
# -*- coding: utf-8 -*-

__all__ = ['Comment', 'Script', 'Keyword', 'Property', 'PropertyValue',
           'Rule', 'Document', 'TokenNotRecognizedError', 'FormatError']
__version__ = '$Id$'

from jsCSS.tokenizer import Tokenizer

class _Leaf(object):
    """Leafs are DOM nodes with no children"""

    def __init__(self, value):
        self.value = value
        self._position = { 'start': None, 'end': None }

    def has_children(self):
        return False

    def get_position(self):
        return self._position

    def set_position(self, start, end):
        self._position = { 'start': start, 'end': end }

    def __str__(self):
        return '<jsCSS.dom.%s "%s" @ %d>' % (self.__class__.__name__, \
                                             "" if self.value is None else self.value if len(self.value) < 72 else self.value[:72] + '...', \
                                             self._position['start'] or 0, )


class Comment(_Leaf):
    """A CSS comment block is represented by a Comment DOM leaf"""
    pass


class Script(_Leaf):
    """A script block is represented by a Script DOM leaf"""
    pass


class Keyword(_Leaf):
    """A CSS keyword is represented by a Keyword DOM leaf"""

    def __init__(self, name, value, options=[]):
        """Initialize a DOM Keyword leaf with the given name and options such
        as !preserve"""
        self.name = name
        self.options = options

        _Leaf.__init__(self, value)

    def __str__(self):
        return '<jsCSS.dom.%s %s "%s" @ %d>' % (self.__class__.__name__, \
                                                self.name, \
                                                self.value, self._position['start'] or 0, )


class PropertyValue(_Leaf):
    """A CSS property is represented by a PropertyValue DOM leaf"""
    pass


class _Node(_Leaf):
    """DOM node with children"""

    def __init__(self, value):
        self._children = []

        _Leaf.__init__(self, value)

    def has_children(self):
        return True

    def append_child(self, child):
        self._children.append(child)

    def get_children(self):
        return self._children


class Property(_Node):
    """A CSS property is represented by a Property DOM node"""

    def __init__(self, name, value):
        self._values = []

        if value is not None:
            if isinstance(value, PropertyValue):
                self.append_value(value)
            else:
                self.append_value(PropertyValue(value))

        _Node.__init__(self, name)

    def append_value(self, value):
        self._values.append(value)
        self.append_child(value)

    def get_values(self):
        return self._values


class Rule(_Node):
    """A CSS rule is represented by a Rule DOM node. If multiple rules are
    defined in one place, those will not be expanded."""

    def __init__(self, rule):
        self._properties = {}

        _Node.__init__(self, rule)

    def append_property(self, property):
        if not property.value in self._properties:
            self._properties[property.value] = [property]
        else:
            self._properties[property.value].append(property)
        self.append_child(property)

    def get_properties(self):
        return self._properties


class TokenNotRecognizedError(Exception):
    """Raised by Document when an unrecognized token is encountered"""

    def __init__(self, type, position):
        self.type = type
        self.position = position

    def __str__(self):
        return "Unrecognized token '%s' @ %d" % (self.type, self.position or 0, )


class FormatError(Exception):
    """Raised by Document when a formatting error is encountered"""

    def __init__(self, message, position):
        self.message = message
        self.position = position

    def __str__(self):
        return "%s @ %d" % (self.message, self.position or 0, )


class Document(_Node):
    """CSS global namespace"""

    def __init__(self):
        _Node.__init__(self, None)

    def from_string(self, string):
        """Populate the document model from a string"""
        tokenizer = Tokenizer()
        return self.from_tokens(tokenizer.tokenize_string(string))

    def from_tokens(self, tokens):
        """Populate the document model from tokens"""
        node, node_stack = self, []
        self._children = []
        last_child = None
        for token in tokens:
            (type, value, start, end, ) = token
            child = self.create_child(type, value, start)

            # Step one level up
            if child == False:
                if len(node_stack) < 1:
                    raise FormatError("'}' without a matching '{'", start)
                node = node_stack.pop()
            else:
                child.set_position(start, end)

                # Step one level down
                if isinstance(child, _Node):
                    if isinstance(child, Property):
                        if isinstance(node, Rule):
                            node.append_property(child)
                        else:
                            raise FormatError("Unexpected property inside %s" % (node, ), start)
                    else:
                        node.append_child(child)
                    node_stack.append(node)
                    node = child
                elif isinstance(child, PropertyValue):
                    if isinstance(last_child, Property):
                        node.append_value(child)
                        node = node_stack.pop()
                    else:
                        raise FormatError("Unexpected property value following %s" % (last_child, ), start)
                else:
                    node.append_child(child)

            if not isinstance(child, (Comment, Script, )):
                last_child = child

        return self

    def create_child(self, type, value, position=None):
        """Create a DOM child for the given type"""
        if type == 'RULE':
            return Rule(value)
        elif type == 'PROPERTY':
            return Property(value, None)
        elif type == 'VALUE':
            return PropertyValue(value)
        elif type == 'COMMENT':
            return Comment(value)
        elif type == 'SCRIPT':
            return Script(value)
        elif type == 'KEYWORD':
            parts = value.split(' ')
            if len(parts) < 2:
                raise FormatError("Keyword '%s' must have at least one argument" % (value, ), position)
            return Keyword(parts[0], parts[1], parts[2:])
        elif type == 'END':
            return False

        raise TokenNotRecognizedError(type, position)

    def __str__(self):
        return '<jsCSS.dom.%s @ %d>' % (self.__class__.__name__, \
                                        self._position['start'] or 0, )
